/*******************************************************************************
 * Copyright 2011-2012 Dik Grapendaal
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/
package sh.grapendaal.tsuushin.module;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import sh.grapendaal.tsuushin.message.IncomingMessage;
import sh.grapendaal.tsuushin.message.MessageType;
import sh.grapendaal.tsuushin.service.PermissionService;
import sh.grapendaal.tsuushin.service.Service;
import sh.grapendaal.tsuushin.tracking.Context;

public class ModuleContainerImpl implements ApplicationContextAware, ModuleContainer {
	private static final Logger logger = LoggerFactory.getLogger(ModuleContainerImpl.class);

	private ApplicationContext applicationContext;
	private ApplicationContext moduleContext;
	private Class<?> mainClass;
	private Object instance;
	private Method startMethod;
	private Method stopMethod;
	private Map<Class<? extends Service>, Service> serviceProxies = new HashMap<Class<? extends Service>, Service>();
	private List<Field> serviceWires = new ArrayList<Field>();
	private Map<MessageType, List<Method>> callbacks = new HashMap<MessageType, List<Method>>();

	@Override
	public void setApplicationContext(ApplicationContext applicationContext) {
		this.applicationContext = applicationContext;
	}

	@Override
	public void setModuleContext(ClassPathXmlApplicationContext moduleContext) {
		this.moduleContext = moduleContext;
	}

	@Override
	public Class<?> getMainClass() {
		return mainClass;
	}

	@Override
	public void setMainClass(Class<?> mainClass) {
		this.mainClass = mainClass;
	}

	@Override
	public Method getStartMethod() {
		return startMethod;
	}

	@Override
	public void setStartMethod(Method startMethod) {
		this.startMethod = startMethod;
	}

	@Override
	public Method getStopMethod() {
		return stopMethod;
	}

	@Override
	public void setStopMethod(Method stopMethod) {
		this.stopMethod = stopMethod;
	}

	@Override
	public void instantiate() throws Exception {
		if (mainClass == null) {
			throw new Exception("No main class configured.");
		}

		try {
			instance = mainClass.newInstance();

			this.wireServices();

			if (startMethod != null) {
				this.invokeMethod(startMethod, null);
			}
		} catch (InstantiationException e) {
			throw new Exception(e);
		} catch (IllegalAccessException e) {
			throw new Exception(e);
		}
	}

	@Override
	public void destroy() {
		if (stopMethod != null) {
			this.invokeMethod(stopMethod, null);
		}

		this.unwireServices();
		this.unregisterPermissions();

		serviceWires.clear();
		callbacks.clear();
		instance = null;
	}

	@Override
	public void registerPermission(String permission) {
		PermissionService permissionService = this.getService(PermissionService.class);
		permissionService.registerPermission(permission);
	}

	@Override
	public void addServiceWire(Field field) {
		serviceWires.add(field);
	}

	@Override
	public void addCallback(Method method, MessageType messageType) {
		if (!callbacks.containsKey(messageType)) {
			callbacks.put(messageType, new ArrayList<Method>());
		}

		callbacks.get(messageType).add(method);
	}
	
	@Override
	public void dispatchCallbacks(Context context, IncomingMessage message) {
		List<Method> methods = callbacks.get(message.getMessageType());
		Object[] availableParameters = new Object[] { context, message };
		if (methods != null) {
			for (Method method : methods) {
				Class<?>[] parameterTypes = method.getParameterTypes();
				Object[] parameters = new Object[parameterTypes.length];
				for (int i = 0; i < parameterTypes.length; i++) {
					parameters[i] = determineParameter(parameterTypes[i], availableParameters);
				}

				this.invokeMethod(method, parameters);
			}
		}
	}

	private void setField(Field field, Object value) {
		try {
			boolean accessible = field.isAccessible();
			field.setAccessible(true);
			
			field.set(instance, value);
			
			field.setAccessible(accessible);
		} catch (IllegalArgumentException e) {
			logger.error("Illegal argument exception while setting {}", field.getName(), e);
		} catch (IllegalAccessException e) {
			logger.error("Illegal access exception while setting {}", field.getName(), e);
		}
	}

	private void invokeMethod(Method method, Object[] parameters) {
		if (parameters == null) {
			parameters = new Object[] {};
		}

		try {
			method.invoke(instance, parameters);
		} catch (IllegalArgumentException e) {
			logger.error("Illegal argument exception while invoking {}", method.getName(), e);
		} catch (IllegalAccessException e) {
			logger.error("Illegal access exception while invoking {}", method.getName(), e);
		} catch (InvocationTargetException e) {
			logger.error("Invocation target exception while invoking {}", method.getName(), e);
		}
	}

	private Object determineParameter(Class<?> neededType, Object[] availableObjects) {
		for (Object object : availableObjects) {
			if (neededType.isInstance(object)) {
				return object;
			}
		}
		logger.warn("Could not determine parameter for {}, available: {}", neededType, availableObjects);
		return null;
	}

	@SuppressWarnings("unchecked")
	private void wireServices() {
		for (Field field : serviceWires) {
			try {
				// Cast is needed for getServiceProxy()
				Class<? extends Service> type = (Class<Service>) field.getType();
				this.setField(field, this.getService(type));
			} catch (NoSuchBeanDefinitionException e) {
				logger.error("Could not wire requested service {}.", field.getType());
			} catch (Exception e) {
				logger.error("Exception occurred while trying to wire service of type {}.", field.getType(), e);
			}
		}
	}

	private void unwireServices() {
		for (Field field : serviceWires) {
			this.setField(field, null);
		}
	}
	
	private void unregisterPermissions() {
		PermissionService permissionService = this.getService(PermissionService.class);
		for (String permission : permissionService.getUsablePermissions()) {
			permissionService.unregisterPermission(permission);
		}
	}
	
	@SuppressWarnings("unchecked")
	private <T extends Service> T getService(Class<T> type) {
		if (serviceProxies.containsKey(type)) {
			return (T) serviceProxies.get(type);
		}
		
		T bean = null;
		for (T foundBean : moduleContext.getBeansOfType(type).values()) {
			if (foundBean instanceof ServiceModuleProxy) {
				bean = foundBean;
			}
		}

		if (bean == null) {
			// No proxy found. Use direct reference to service instead.
			bean = applicationContext.getBean(type);
		}
		
		serviceProxies.put(type, bean);
		
		return bean;
	}
}
